package com.zy.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.function.Function;

import static com.zy.utils.RedisConstants.*;

/**
 * [缓存工具类]
 *
 * @author : [张宇]
 * @version : [v1.0]
 * @createTime : [2024/12/15 10:51]
 */
@Component
@Slf4j
public class CacheClient {

    // 使用Spring的线程池
    @Autowired
    private ThreadPoolTaskExecutor taskExecutor;

    private final StringRedisTemplate stringRedisTemplate;

    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public void set (String key, Object value, Duration timeout){
        stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(value), timeout);
    }

    public void setWithLogicalExpire(String key, Object value, Duration timeout){
        // 设置逻辑过期
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeout.getSeconds()));
        // 写入redis
        stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(redisData));
    }

    /**
     * 缓存穿透
     * @param id
     * @return
     */
    public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Duration timeout){
        String key = keyPrefix + id;
        // 1.从redis查询缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在,判断是不是有值
        if (StrUtil.isNotBlank(json)) {
            // 3.存在,直接返回
            return JSON.parseObject(json, type);
        }
        //判断是不是null,这时候是空字符串(两种情况,null和空字符串)
        if(json != null){
            return null;
        }
        // 4.不存在,根据id查询数据库
        R apply = dbFallback.apply(id);
        // 5.不存在,返回404
        if (apply == null) {
            // 将null值写入缓存
            stringRedisTemplate.opsForValue().set(key, "", Duration.ofMinutes(CACHE_NULL_TTL));
            return null;
        }
        // 6.存在,写入redis
        set(key, apply, timeout);
        return apply;
    }

    /**
     * 逻辑过期解决缓存击穿问题
     * @param id
     * @return
     */
    public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Duration timeout){
        // 1.从redis查询缓存,查询出来的是RedisData
        String redisDataJson = stringRedisTemplate.opsForValue().get(keyPrefix + id);
        // 2.判断是否命中
        if (StrUtil.isBlank(redisDataJson)) {
            // 3.没命中,直接返回空
            return null;
        }
        // 4.命中,将redisData转为LocalDateTime和Shop对象.json反序列化为对象
        JSONObject redisData = JSON.parseObject(redisDataJson);
        R r = redisData.getObject("data", type); // 使用 getObject 方法进行类型转换
        LocalDateTime expireTime = redisData.getObject("expireTime", LocalDateTime.class); // 同样使用 getObject 方法
        // 5.判断缓存逻辑过期时间是否过期
        if (BooleanUtil.isTrue(expireTime.isAfter(LocalDateTime.now()))) {
            // 6.未过期,直接返回店铺信息
            return r;
        }
        // 7.过期尝试获取互斥锁
        boolean flag = tryLock(LOCK_SHOP_KEY + id);
        // 7.1获取成功,开启独立线程,返回商铺信息
        if(flag){
            taskExecutor.execute(() -> {
                try {
                    Thread.sleep(200);
                    // 重建缓存逻辑 1.查数据库
                    R apply = dbFallback.apply(id);
                    // 2.写入redis
                    setWithLogicalExpire(keyPrefix + id, apply, timeout);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    unLock(LOCK_SHOP_KEY + id);
                }
            });
        }
        return r;
    }

    /**
     * 尝试获取锁
     * @param key
     * @return
     */
    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", Duration.ofSeconds(LOCK_SHOP_TTL));
        //Boolean为包装类型,需要拆箱,boolean为基本类型,拆箱可能出现空指针
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 释放锁
     * @param key
     */
    private void unLock(String key){
        stringRedisTemplate.delete(key);
    }


}
